package jmpo.gn;

/**
 * Дженерики (Generics) или обобщённые классы, или обобщения - это 
 * специальные классы, которые позволяют убрать явное определение типа ссылки
 *  и работать со ссылками, не уточняятип элемента, на который они указывают. 
 * Такие классы значительно расширяют возможности переиспользование кода.
 */
/**
 * Наиболее частое использование обобщений - это различные алгоритмы и 
 * структуры данных, которые могут быть реализованы без точного знания 
 * типа элемента. Рассмотрим класс StringPair, который принимает два объекта в конструкторе
 * и возвращает первый из них в методе first, а второй к методе second.
 */
final class StringPair {
    StringPair(String lha, String rha) {
        this.lha = lha;
        this.rha = rha;
    }

    String first() { return lha; }
    String second() { return rha; }

    private String lha;
    private String rha;
}

/**
 * Описанный выше класс не привязан чётко к типу String. Если мы создадим 
 * класс PairInteger, то он будет абсолютно таким же.
 * Чтобы не дублировать классы, вспомним, что все они наследуются от 
 * Object. Тогда мы можем в классе универсальной пары удерживать ссылки на 
 * два Object. Такой подход позволяет нам даже сохранять пары различных элементов.
 */
final class Pair {
    Pair(Object lha, Object rha) {
        this.lha = lha;
        this.rha = rha;
    }

    Object first() { return lha; }
    Object second() { return rha; }

    private Object lha;
    private Object rha; 
}

/**
 * Такой подход приводит к необходимости постоянно помнить какие типы мы 
 * сохраняли в первом и втором элементе пары. Затем приводить типы вручную.
 * Такой код подвержен случайным ошибкам. При этом они будут обнаружены только 
 * на этапе исполнения, если код попадёт в определённую ветку с неудачными параметрами.
 * На этапе тестирования отловить такие ошибки может быть очень сложно.
 */
/**
 * Начиная с версии Java 5 в Java появилась возможность переложить ответственность
 * по преобразованию типов на компилятор с помощью специального синтаксиса.
 */
final class GenericPair<T,U> {
    GenericPair(T t, U u) {
        this.t = t;
        this.u = u;
    }

    T first() { return t; }
    U second() { return u; }

    private T t;
    private U u;
}

/**
 * По существу GenericPair - это ровно тоже самое, что и Pair с Object, 
 * но теперь компилятор будет сам преобразовывать типы.
 * В коде вида
 *     GenericPair<String,Ingeger> p = new GenericPair<String,Integer>("Hello", 5);
 *     var s = p.first();
 * мы получим доступ ко всем методам String, потому что компилятор сделать приведение
 * типа для нас.
 * Но стоит помнить, что есть только один класс GenericPair.
 * Такая техника обобщения называется type erasure (вытирание типов).
 * Т.е. на этапе исполнения не будет никакой возможности узнать какие именно типы
 * хранит p.
 */

public class SimpleGenerics {
    private static boolean objectPairTest() {
        var p = new Pair(new String("Hello"), 5);
        var s = (String)p.first();
        var i = (Integer)p.second();
        return s.equals("Hello") && i.equals(5);
    }

    private static boolean genericPairTestA() {
        GenericPair<String, Integer> p = new GenericPair<String,Integer>("Hello", 5);
        var s = p.first();
        return s.startsWith("H");

    }

    private static boolean genericPairTestB() {
        var p = new GenericPair<String, Integer>("Hello", 5);
        var s = p.first();
        return s.startsWith("H");
    }

    private static void test(boolean expectedTrue) {
        if (expectedTrue) {
            System.out.println("Ok!");
            return;
        }
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        test(objectPairTest());
        test(genericPairTestA());
        test(genericPairTestB());
    }
    
}
